FRIDA辅助分析 OLLVM 字符串加密
OLLVM 字符串加密的详细实现原理
字符串加密是 OLLVM 混淆中最基础也是最实用的 pass 之一。它的目标是将 SO 文件中的明文字符串转换为密文,使得静态分析工具(如 IDA 的 Strings 窗口、strings 命令)无法直接读取敏感信息。
加密流程
OLLVM 字符串加密在 LLVM 编译后端工作,具体流程如下:
源码中的字符串: "https://api.example.com/token"
↓ LLVM IR 阶段
识别所有 GlobalVariable 类型的字符串常量
↓
对字符串进行加密(XOR / 旋转 / 替换)
↓
生成加密后的全局数组: [0x7F, 0x72, 0x69, 0x63, ...]
↓
生成解密函数: __decrypt_str_N()
↓
在所有引用原始字符串的位置插入解密函数调用
↓
最终二进制: 密文数据 + 解密函数 + 调用点
常见的加密方式
- 固定密钥 XOR:最简单的方式,所有字符串使用同一个 XOR 密钥
- 逐字节递增 XOR:密钥逐字节递增(如 key = 0x5A, 0x5B, 0x5C, …)
- 滚动 XOR:每个字节的密钥依赖前一个字节的加密结果
- 混合变换:XOR + 位移 + 加法的组合
加密函数在 SO 中的特征
_Z 前缀加密函数
OLLVM 生成的解密函数遵循 C++ 的 name mangling 规则,以 _Z 开头。在 IDA 中可以通过以下方式快速定位:
// 枚举 SO 中所有以 _Z 开头的函数
function findEncryptedFunctions(moduleName) {
var mod = Process.findModuleByName(moduleName);
if (!mod) {
console.log("[-] 模块未加载: " + moduleName);
return;
}
console.log("[*] 模块: " + mod.name +
" 基地址: " + mod.base);
console.log("[*] 开始扫描加密函数...\n");
// 枚举所有导出符号
var symbols = mod.enumerateSymbols();
var candidates = [];
symbols.forEach(function (sym) {
// 过滤以 _Z 开头且可能是解密函数的符号
if (sym.name.indexOf("_Z") === 0) {
// 加密函数通常名称较长(包含编码后的信息)
if (sym.name.length > 30) {
candidates.push({
name: sym.name,
address: sym.address,
type: sym.type
});
}
}
});
console.log("[+] 找到 " + candidates.length +
" 个可能的解密函数:");
candidates.forEach(function (c, i) {
console.log(" " + (i + 1) + ". " + c.name);
console.log(" 地址: " + c.address);
});
return candidates;
}
var candidates = findEncryptedFunctions("libnative.so");
解密函数的代码特征
在 IDA 反汇编视图中,解密函数通常有以下代码模式:
// 典型的 XOR 解密函数伪代码
char* __decrypt_str_42() {
static char decrypted[32];
static int initialized = 0;
if (!initialized) {
// XOR 密文字节
char key = 0x5A;
for (int i = 0; i < 32; i++) {
decrypted[i] = encrypted_data[i] ^ key;
key += 3; // 递增密钥
}
initialized = 1;
}
return decrypted;
}
Hook 所有字符串操作函数
除了直接 Hook 解密函数外,还可以 Hook C 标准库中的字符串操作函数来捕获运行时的字符串使用:
Hook strcmp / strncmp
// Hook strcmp 捕获所有字符串比较
function hookStringCompare() {
var strcmpAddr = Module.findExportByName("libc.so", "strcmp");
var strncmpAddr = Module.findExportByName("libc.so", "strncmp");
Interceptor.attach(strcmpAddr, {
onEnter: function (args) {
var s1 = args[0].readCString();
var s2 = args[1].readCString();
if (s1 && s2 && s1.length > 0 && s2.length > 0 &&
s1.length < 256 && s2.length < 256) {
console.log("[strcmp] \"" + s1 + "\" vs \"" + s2 + "\"");
// 打印调用栈
var bt = Thread.backtrace(this.context,
Backtracer.FUZZY).slice(0, 3);
bt.forEach(function (addr) {
console.log(" ← " + DebugSymbol.fromAddress(addr));
});
}
}
});
Interceptor.attach(strncmpAddr, {
onEnter: function (args) {
var s1 = args[0].readCString();
var s2 = args[1].readCString();
var n = args[2].toInt32();
if (s1 && s2 && n < 256) {
console.log("[strncmp] \"" +
(s1 || "").substring(0, n) + "\" vs \"" +
(s2 || "").substring(0, n) + "\" (n=" + n + ")");
}
}
});
console.log("[+] strcmp/strncmp Hook 已设置");
}
hookStringCompare();
Hook strlen / memcpy / memset
function hookStringOps() {
// Hook strlen - 可以发现被解密后的字符串长度
var strlenAddr = Module.findExportByName("libc.so", "strlen");
Interceptor.attach(strlenAddr, {
onEnter: function (args) {
try {
var s = args[0].readCString();
if (s && s.length > 3 && s.length < 512) {
// 过滤只看有意义的字符串
var isPrintable = /^[\x20-\x7E]+$/.test(s);
if (isPrintable) {
console.log("[strlen] \"" + s +
"\" (len=" + s.length + ")");
}
}
} catch (e) {}
}
});
// Hook memcmp - 常用于签名校验和密钥比较
var memcmpAddr = Module.findExportByName("libc.so", "memcmp");
Interceptor.attach(memcmpAddr, {
onEnter: function (args) {
var n = args[2].toInt32();
if (n > 0 && n <= 32) {
console.log("[memcmp] 比较长度: " + n + " 字节");
console.log(" ptr1: " + hexdump(args[0], { length: n }));
console.log(" ptr2: " + hexdump(args[1], { length: n }));
}
},
onLeave: function (retval) {
if (retval.toInt32() === 0) {
console.log(" [memcmp] 匹配成功!");
}
}
});
}
hookStringOps();
批量 Dump 解密后的字符串
自动化 Dump 脚本
// 批量 dump 解密字符串的完整脚本
var StringDumper = {
strings: [],
dumpCount: 0,
// 方案 1: Hook OLLVM 解密函数
hookDecryptFunc: function (moduleName, decryptOffset) {
var mod = Process.findModuleByName(moduleName);
var addr = mod.base.add(decryptOffset);
Interceptor.attach(addr, {
onLeave: function (retval) {
this.collectString(retval);
}
}.bind(this));
console.log("[+] 已 Hook 解密函数: " + addr);
},
// 方案 2: Hook JNI NewStringUTF(捕获 Java 层可见的字符串)
hookJNINewString: function () {
var libart = Process.findModuleByName("libart.so");
if (!libart) {
console.log("[-] libart.so 未加载");
return;
}
// 枚举 libart 的导出符号找到 NewStringUTF
var symbols = libart.enumerateExports();
symbols.forEach(function (sym) {
if (sym.name.indexOf("NewStringUTF") !== -1) {
console.log("[+] 找到: " + sym.name + " @ " + sym.address);
try {
Interceptor.attach(sym.address, {
onEnter: function (args) {
try {
var str = args[1].readCString();
if (str && str.length > 0 &&
str.length < 1024) {
this.collectString(args[1]);
}
} catch (e) {}
}
}.bind(this));
} catch (e) {}
}
}.bind(this));
},
// 收集字符串
collectString: function (ptr) {
try {
var str = ptr.readCString();
if (str && str.length > 2 && str.length < 1024) {
this.strings.push({
str: str,
addr: ptr.toString(),
timestamp: Date.now()
});
this.dumpCount++;
}
} catch (e) {}
},
// 输出结果
print: function () {
console.log("\n======== 字符串 Dump 结果 ========");
console.log("共收集 " + this.strings.length + " 个字符串\n");
// 按字符串内容去重排序
var unique = {};
this.strings.forEach(function (item) {
if (!unique[item.str]) {
unique[item.str] = item;
}
});
var sorted = Object.values(unique).sort(function (a, b) {
return a.str.localeCompare(b.str);
});
// 分类输出
var urls = sorted.filter(function (s) {
return /^https?:\/\//i.test(s.str);
});
var keys = sorted.filter(function (s) {
return /key|secret|token|password|sign/i.test(s.str);
});
var paths = sorted.filter(function (s) {
return /^\//.test(s.str);
});
var others = sorted.filter(function (s) {
return !urls.concat(keys, paths).some(function (u) {
return u.str === s.str;
});
});
if (urls.length > 0) {
console.log("[URL]");
urls.forEach(function (u) {
console.log(" " + u.str);
});
}
if (keys.length > 0) {
console.log("[密钥/敏感]");
keys.forEach(function (k) {
console.log(" " + k.str);
});
}
if (paths.length > 0) {
console.log("[路径]");
paths.forEach(function (p) {
console.log(" " + p.str);
});
}
if (others.length > 0) {
console.log("[其他]");
others.forEach(function (o) {
console.log(" " + o.str);
});
}
console.log("\n[统计] URL: " + urls.length +
" 密钥: " + keys.length +
" 路径: " + paths.length +
" 其他: " + others.length);
}
};
// 使用
StringDumper.hookDecryptFunc("libnative.so", 0x2A10);
StringDumper.hookJNINewString();
// 触发 APP 操作后执行
// StringDumper.print();
构建字符串解密辅助脚本
通用解密函数 Hook 模板
// 通用 OLLVM 字符串解密辅助脚本
// 使用方式: 根据实际 APP 修改 moduleName 和 offsets
var CONFIG = {
moduleName: "libnative.so",
// 在 IDA 中找到的解密函数偏移列表
decryptFunctions: [0x2A10, 0x2B40, 0x2C80],
// 需要过滤的关键词(只显示包含这些词的字符串)
filterKeywords: ["http", "key", "token", "sign", "secret",
"password", "api", "encrypt", "decrypt", "/"],
// 最大字符串长度限制
maxLength: 512
};
function createDecryptionHooker(config) {
var mod = Process.findModuleByName(config.moduleName);
if (!mod) {
console.log("[-] 模块 " + config.moduleName + " 未加载");
return null;
}
var collected = [];
config.decryptFunctions.forEach(function (offset) {
var addr = mod.base.add(offset);
try {
Interceptor.attach(addr, {
onEnter: function (args) {
// 可选: 记录输入参数(密文)
this._encrypted = args[0];
},
onLeave: function (retval) {
try {
var str = retval.readCString();
if (!str || str.length === 0 ||
str.length > config.maxLength) return;
// 检查是否可打印
var printable = true;
for (var i = 0; i < Math.min(str.length, 50); i++) {
if (str.charCodeAt(i) < 0x20 &&
str.charCodeAt(i) !== 0x09 &&
str.charCodeAt(i) !== 0x0A) {
printable = false;
break;
}
}
if (!printable) return;
// 关键词过滤
var shouldLog = config.filterKeywords.length === 0;
if (!shouldLog) {
for (var k = 0; k < config.filterKeywords.length; k++) {
if (str.toLowerCase().indexOf(
config.filterKeywords[k]) !== -1) {
shouldLog = true;
break;
}
}
}
if (shouldLog) {
console.log("[解密] 0x" +
offset.toString(16) + " → \"" + str + "\"");
var bt = Thread.backtrace(this.context,
Backtracer.FUZZY).slice(0, 2);
var callerInfo = bt.map(function (a) {
return DebugSymbol.fromAddress(a).toString();
}).join(" ← ");
console.log(" 调用: " + callerInfo);
collected.push(str);
}
} catch (e) {}
}
});
console.log("[+] Hook 解密函数 0x" + offset.toString(16));
} catch (e) {
console.log("[-] Hook 失败 0x" + offset.toString(16) +
": " + e);
}
});
return {
getAll: function () { return collected; },
printUnique: function () {
var unique = [...new Set(collected)];
console.log("\n=== 唯一字符串 (" + unique.length + ") ===");
unique.forEach(function (s) { console.log(s); });
}
};
}
var hooker = createDecryptionHooker(CONFIG);
实际案例分析
案例:分析某加固 APP 的 SO 字符串加密
假设我们面对一个使用 OLLVM 字符串加密的 APP,分析步骤如下:
第一步:在 IDA 中定位解密函数
- 打开
libnative.so,查看 Strings 窗口,发现几乎没有可读字符串 - 查看导出函数列表,找到以
_Z开头的长名称函数 - 交叉引用分析,确认哪些函数被大量调用且返回值被当作字符串使用
第二步:Frida Hook 验证
// 验证找到的解密函数
var mod = Process.findModuleByName("libnative.so");
var candidates = [0x2A10, 0x2B40, 0x2C80];
candidates.forEach(function (offset) {
var addr = mod.base.add(offset);
Interceptor.attach(addr, {
onLeave: function (retval) {
try {
var str = retval.readCString();
if (str && str.length > 0 && str.length < 256) {
var hex = "";
for (var i = 0; i < Math.min(str.length, 16); i++) {
hex += ("0" + str.charCodeAt(i).toString(16))
.slice(-2) + " ";
}
console.log("[0x" + offset.toString(16) +
"] len=" + str.length +
" hex=" + hex.trim() +
" str=\"" + str.substring(0, 50) + "\"");
}
} catch (e) {}
}
});
});
第三步:批量收集并分类
触发 APP 的各个功能模块(登录、支付、数据同步等),收集所有解密后的字符串,按照 URL、密钥、路径等分类存储,为后续的协议分析和算法还原提供线索。
总结
OLLVM 字符串加密通过在编译时加密、运行时解密的方式保护 SO 文件中的敏感字符串。使用 Frida 动态 Hook 解密函数或 libc 字符串操作函数,可以在运行时批量恢复所有明文字符串。这种方法不依赖静态分析对混淆的去除,是应对字符串加密最直接有效的手段。结合自动化脚本和关键词过滤,可以快速提取出 APP 中的 API 地址、加密密钥等关键信息。